Learn C++ by Example by Frances Buontempo

Learn C++ by Example by Frances Buontempo

Author:Frances Buontempo [Buontempo, Frances]
Language: eng
Format: epub
Publisher: Manning Publications Co.
Published: 2024-02-22T23:00:00+00:00


5.2.3 Using std::variant to support cards or jokers

The simplest way to define a joker is as an empty struct.

Listing 5.26 A joker

struct Joker { };

That is all we need.

We know how to make a deck of 52 playing cards:

std::array<Card, 52> cards = create_deck();

How do we add two jokers? We can’t add jokers to this deck because they are a different type. We could make a common base type and use pointers for dynamic polymorphism, but that seems over the top. A much simpler approach would be an array of one of two types: cards or jokers. The std::variant, introduced in C++17, makes this possible. It lives in the variant header and behaves like a union, but it is safer. C’s union type has a sequence of possible members.

Listing 5.27 A union

union CardOrJoker { Card card; Joker joker; };

The union is big enough to hold the largest type used. To access a Card from this union, you use the card member, and for a Joker, use the joker member, but you need to track which type is in use. In contrast, a variant knows which type it currently holds, so the variant is often described as a type-safe union.

We declare a variant by stating which types it can hold:

std::variant<Card, Joker>

The variant is a class template defined as a variadic template. We will look at these in more detail in the last chapter, but for now, notice the three dots in the definition:

template <class... Types> class variant;

The dots are called a parameter pack, allowing us to use zero or more template arguments. This allows us to define a variant with the two types we need. We used std::optional in chapter 3 to handle input, which only needed one type. Declaring an optional without assigning a value

std::optional<Card> card;

has no value. If we use this card in a Boolean context, it will evaluate to false, so we could make an optional work, but the code might be hard to follow. We’d need to remember that if(!card) meant we had a joker. How do we use a variant, then?

A variant is initialized to the first of the alternative types, provided that type can be default constructed. If it can’t, we get a compile error. Both of our types can be default constructed, so that won’t happen here. So using

std::variant<Card, Joker> card;

gives us a default constructed Card because that’s the first type. We could also create a Joker instead:

std::variant<Card, Joker> joker{ Joker{} };

In fact, there are various ways to create a variant. We can avoid making a temporary Joker{} to construct the variant using the std::in_place_index function. For a Joker, we want index 1 and do not have any arguments for the joker’s constructor, so we’ll use std::in_place_index with value 1:

std::variant<Card, Joker> joker2(std::in_place_index<1>);

For a Card, we use the zero index and pass the value and suit to the Card constructor:

std::variant<Card, Joker> two_of_clubs(std::in_place_index<0>, FaceValue(2), Suit::Clubs);

For further details, see http://mng.bz/amzY.

We can determine whether we have a joker by checking the variant’s type:

bool is_joker = std::holds_alternative<Joker>(two_of_clubs);

There are various ways to retrieve the values.



Download



Copyright Disclaimer:
This site does not store any files on its server. We only index and link to content provided by other sites. Please contact the content providers to delete copyright contents if any and email us, we'll remove relevant links or contents immediately.